/*
 * Copyright 2009 IT Mill Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.nafets;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vaadin.hezamu.canvas.Canvas;
import org.vaadin.hezamu.canvas.Canvas.CanvasMouseDownListener;

import com.nafets.util.CanvasUtils;
import com.nafets.util.MathUtils;
import com.vaadin.Application;
import com.vaadin.ui.AbsoluteLayout;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.themes.Reindeer;

/**
 * The Application's "main" class
 */
@SuppressWarnings("serial")
public class GuitarExerciseApplication extends Application {
	private static final String GUESS_THE_NOTE = "Guess the note \"%s\"";
	private Window window;
	private Canvas baseCanvas;
	private Canvas stringCanvas;
	private Canvas topCanvas;
	private Label result;
	private Label title;

	private Tone[] initialTones = { Tone.E, Tone.A, Tone.D, Tone.G, Tone.B };
	private SimpleGameModel gameModel;
	private GuitarString[] strings;
	private Set<Point> points;
	private double[] fretY;

	private static final Logger log = LoggerFactory.getLogger(GuitarExerciseApplication.class);

	@Override
	public void init() {
		initialize();

		Layout guitarLayout = guitarLayout(300, 300);
		Layout buttonsLayout = buttonsLayout();

		VerticalLayout mainLayout = new VerticalLayout();
		mainLayout.setSpacing(true);
		mainLayout.addComponent(title);
		mainLayout.addComponent(guitarLayout);
		mainLayout.addComponent(buttonsLayout);
		mainLayout.addComponent(result);
		mainLayout.setComponentAlignment(title, Alignment.MIDDLE_CENTER);
		mainLayout.setComponentAlignment(guitarLayout, Alignment.MIDDLE_CENTER);
		mainLayout.setComponentAlignment(buttonsLayout, Alignment.MIDDLE_CENTER);
		mainLayout.setComponentAlignment(result, Alignment.MIDDLE_CENTER);

		window = new Window("Guitar Exercise");
		window.addComponent(mainLayout);
		setMainWindow(window);
		setTheme("chameleon");
	}

	private void initialize() {
		points = new HashSet<Point>();

		baseCanvas = new Canvas();
		stringCanvas = new Canvas();
		topCanvas = new Canvas();

		drawNeck(baseCanvas, 300, 300, 6, 12, 20, 10, 5);
		drawStrings(baseCanvas, 300, 300, 6, 12, 20, 10, 5);
		drawDynamicCanvas(300, 300, 10, topCanvas);

		title = new Label();
		title.addStyleName(Reindeer.LABEL_H1);
		title.setSizeUndefined();
		title.setValue(String.format(GUESS_THE_NOTE, gameModel.toneToGuess().toString().replace('s', '#')));

		result = new Label();
		result.setVisible(false);
		result.setSizeUndefined();
	}

	private Layout guitarLayout(double height, double width) {
		AbsoluteLayout guitarLayout = new AbsoluteLayout();
		guitarLayout.setHeight(height + "px");
		guitarLayout.setWidth(width + "px");
		guitarLayout.addComponent(baseCanvas, "top:0px;left:0px");
		guitarLayout.addComponent(stringCanvas, "top:0px;left:0px");
		guitarLayout.addComponent(topCanvas, "top:0px;left:0px");

		return guitarLayout;
	}

	private Layout buttonsLayout() {
		Button submit = new Button("Submit");
		submit.addListener(new Button.ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				submit();
			}
		});

		Button newGame = new Button("New Game");
		newGame.addListener(new Button.ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				startGame();
			}
		});

		HorizontalLayout buttonsLayout = new HorizontalLayout();
		buttonsLayout.addComponent(submit);
		buttonsLayout.addComponent(newGame);
		buttonsLayout.setSpacing(true);

		return buttonsLayout;
	}
	
	private void submit() {
		String value = gameModel.endGame() ? "You win!" : "You lose!";
		System.out.println(value);
		result.setValue(value);
		result.setVisible(true);

		topCanvas.clear();
		Set<NeckPosition> correct = gameModel.getTonesToFind();
		for (Point p : points) {
			for (int i = 0; i < strings.length; i++) {
				if (strings[i].distanceTo(p.x, p.y) == 0) {
					int fretPos = fretPosition(p.y);

					String fillStyle = correct.contains(new NeckPosition(i, fretPos)) ? "green" : "red";
					CanvasUtils.drawArc(p.x, p.y, 5, (Math.PI / 180) * 0, (Math.PI / 180) * 360, true, topCanvas,
							fillStyle);
				}
			}
		}
	}

	private void startGame() {
		topCanvas.clear();
		points.clear();
		gameModel.startGame();
		title.setValue(String.format(GUESS_THE_NOTE, gameModel.toneToGuess().toString().replace('s', '#')));
		result.setVisible(false);
	}

	private void drawStrings(Canvas canvas, final double height, final double width, final int stringCount,
			final int fretCount, final double stringBottomOffset, final double stringTopOffset, final double frameOffset) {
		
		canvas.setHeight(height + "px");
		canvas.setWidth(width + "px");
		strings = new GuitarString[stringCount];
		// draw the strings
		double x1 = width / 2.0 - (((double) (stringCount - 1)) / 2.0) * stringTopOffset;
		double x2 = width / 2.0 - (((double) (stringCount - 1)) / 2.0) * stringBottomOffset;
		double y1 = frameOffset;
		double y2 = height - frameOffset;

		for (int i = 0, t = 0; i < stringCount; i++, x1 += stringTopOffset, x2 += stringBottomOffset, t++) {
			if (t >= initialTones.length) {
				t = 0;
			}
			strings[i] = new GuitarString(x1, y1, x2, y2, initialTones[t], fretCount);
			canvas.setLineWidth(3 - i * 0.5);
			CanvasUtils.drawLine(x1, y1, x2, y2, "black", canvas);
		}

		gameModel = new SimpleGameModel(stringCount, fretCount, strings);
		gameModel.startGame();
	}

	private void drawNeck(Canvas canvas, final double height, final double width, final int stringCount,
			final int fretCount, final double stringBottomOffset, final double stringTopOffset, final double frameOffset) {

		fretY = new double[fretCount + 1];
		canvas.setHeight(height + "px");
		canvas.setWidth(width + "px");
		canvas.setBackgroundColor("#D2D2D2");
		canvas.setStrokeColor("black");
		canvas.setLineCap("square");

		double x1 = width / 2.0 - (((double) (stringCount - 1)) / 2.0) * stringTopOffset - 4;
		double x2 = width / 2.0 - (((double) (stringCount - 1)) / 2.0) * stringBottomOffset - 4;
		double y1 = frameOffset;
		double y2 = height - frameOffset;

		double x3 = x1 + (stringCount - 1) * stringTopOffset + 8;
		double x4 = x2 + (stringCount - 1) * stringBottomOffset + 8;
		
		double fretLength = MathUtils.lineLength(x1, y1, x2, y2);
		double shortestFretLength = (fretLength / fretCount) * 0.8;
		double delta = 2 * (fretLength - fretCount * shortestFretLength) / (fretCount * (fretCount - 1));

		canvas.setFillStyle(220, 200, 125);
		canvas.beginPath();
		canvas.moveTo(x1, y1);
		canvas.lineTo(x2, y2);
		canvas.lineTo(x3, y1);
		canvas.moveTo(x4, y2);
		canvas.lineTo(x3, y1);
		canvas.lineTo(x2, y2);
		canvas.fill();
		canvas.closePath();
		
		
		canvas.beginPath();
		canvas.setLineWidth(2);
		canvas.setStrokeStyle(128, 128, 0);
		fretY[0] = CanvasUtils.drawLine(x1, y1, x2, y2, x3, y1, x4, y2, fretLength, canvas);
		fretLength -= shortestFretLength;
		canvas.stroke();
		canvas.closePath();
		
		logDebug(String.format("Neck: %f, delta: %f, Shortest fret: %f", fretLength, delta, shortestFretLength));
		
		canvas.setStrokeStyle(128, 128, 0);
		for (int i = 0; i < fretCount; i++, fretLength -= (i * delta + shortestFretLength)) {
			logDebug("Fret length: " + fretLength);
			fretY[i + 1] = CanvasUtils.drawLine(x1, y1, x2, y2, x3, y1, x4, y2, fretLength, canvas);
		}
		
		ArrayUtils.reverse(fretY);
		
		LineCoef leftSide = MathUtils.getLineCoefficients(x1, y1, x2, y2);
		LineCoef rightSide = MathUtils.getLineCoefficients(x3, y1, x4, y2);
		for (int i = 0; i <= fretCount; i++) {
			if (i > 0 && i % 12 == 0) {
				double y5 = (fretY[i] + fretY[i - 1]) / 2;
				double _x1 = MathUtils.getX(leftSide.a, leftSide.b, y5);
				double _x2 = MathUtils.getX(rightSide.a, rightSide.b, y5);
				double offset = (Math.abs(_x1 - _x2) - 8) / (stringCount - 1); 
				double x5 = (_x1 + _x2) / 2;
				CanvasUtils.drawArc(x5 - offset, y5, 5, (Math.PI / 180) * 0,
						(Math.PI / 180) * 360, true, canvas, "white");
				CanvasUtils.drawArc(x5 + offset, y5, 5, (Math.PI / 180) * 0,
						(Math.PI / 180) * 360, true, canvas, "white");
			}
			if (i % 12 == 3 || i % 12 == 5 || i % 12 == 7 || i % 12 == 9) {
				double y5 = (fretY[i] + fretY[i - 1]) / 2;
				CanvasUtils.drawArc((x1 + x3) / 2, y5, 5, (Math.PI / 180) * 0,
						(Math.PI / 180) * 360, true, canvas, "white");
			}
		}
	}

	private void drawDynamicCanvas(double height, double width, final double maxError, Canvas topCanvas) {
		topCanvas.setHeight(height + "px");
		topCanvas.setWidth(width + "px");

		topCanvas.addListener(new CanvasMouseDownListener() {
			@Override
			public void mouseDown(int x, int y) {
				handleClick(x, y, maxError);
			}
		});
	}

	private void handleClick(int x, int y, double maxError) {
		if (!gameModel.getState().equals(State.GAME_IN_PROGRESS)) {
			return;
		}

		// get clicked fret's Y range
		int fretIndex = fretPosition(y);
		if (fretIndex == -1) {
			return;
		}

		double y1 = fretY[fretIndex - 1];
		double y2 = fretY[fretIndex];
		// end

		// now get the reasonably closest string
		int stringIndex = closestStringIndex(x, y, maxError);
		if (stringIndex == -1) {
			log.warn("Click is too far away from the strings, disregarding it, because it " + "is probably a misclick.");
			return;
		}

		// calculate where is the middle of the fret on the string
		LineCoef coef = strings[stringIndex].getLineCoef();
		double y3 = (y1 + y2) / 2;
		double x3 = MathUtils.getX(coef.a, coef.b, y3);

		Point p = new Point(x3, y3);
		if (points.contains(p)) {
			handleRemove(stringIndex, fretIndex, p, topCanvas, "black");
		} else {
			handleSelect(stringIndex, fretIndex, x3, y3, p, topCanvas, "black");
		}
	}

	private void handleSelect(int stringIndex, int fretIndex, double x, double y, Point p, Canvas canvas, String color) {
		gameModel.selectTone(stringIndex, fretIndex);
		CanvasUtils.drawArc(x, y, 5, (Math.PI / 180) * 0, (Math.PI / 180) * 360, true, canvas, color);
		points.add(p);
	}

	private void handleRemove(int stringIndex, int fretIndex, Point p, Canvas canvas, String color) {
		gameModel.deselectTone(stringIndex, fretIndex);
		points.remove(p);
		topCanvas.clear();

		// redraw selected
		for (Point center : points) {
			CanvasUtils.drawArc(center.x, center.y, 5, (Math.PI / 180) * 0, (Math.PI / 180) * 360, true, topCanvas,
					"black");
		}
	}

	private int closestStringIndex(double x, double y, double maxError) {
		int index = 0;
		double minDistance = Double.MAX_VALUE;
		for (int i = 0; i < strings.length; i++) {
			double h = strings[i].distanceTo(x, y);
			if (h < minDistance) {
				minDistance = h;
				index = i;
			}
		}

		return minDistance > maxError ? -1 : index;
	}

	private int fretPosition(double y) {
		int fretIndex = -1;
		for (int i = 0; i < fretY.length - 1; i++) {
			if (y > fretY[i] && y < fretY[i + 1]) {
				fretIndex = i + 1;
				break;
			}
		}

		return fretIndex;
	}

	private void logDebug(String msg) {
		if (log.isDebugEnabled()) {
			log.debug(msg);
		}
	}
}
